/**
 * Jaxson ajax
 *
 * Copyright Remedy Interactive, licensed under Apache version 2 license:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This notice must remain in all java source files.
 */
package org.jaxson.config;


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>
 * Singleton providing access to configuration for a single
 * ajax web application instance.  The configuration parameters
 * are available in java, with a subset being available in javascript.
 * </p>
 * 
 * <p>
 * Client properties must be prefixed with the name "client."
 * and are parsed out into the javascript ApplicationConfiguration object.
 * </p>
 * 
 * @author Joe Maisel
 */
public class ApplicationConfiguration extends Properties
{
	private static final long serialVersionUID = 85940348510941L;
	private long lastModified;
	private String path;
	private String contents;
	private static ApplicationConfiguration instance;
	private static final Log lager = LogFactory.getLog( ApplicationConfiguration.class.getName() );

	private ApplicationConfiguration () throws IOException
	{
		instance = this;
		ServletContext sc = JavascriptConfiguration.getInstance().getServletContext();
		String file = sc.getInitParameter( "application.configuration.file" );
		path = sc.getRealPath( file );
		load( path );
	}
	
	/**
	 * Singleton accessor
	 * @throws IOException if loading the configuration file fails
	 */
	public static ApplicationConfiguration getInstance() throws IOException
	{
		if( instance == null )
			instance = new ApplicationConfiguration();
		
		instance.load(  instance.path  );
		
		return instance;
	}

	/**
	 * @return the path
	 */
	public String getPath()
	{
		return path;
	}

	/**
	 * @param path
	 *            the path to set
	 */
	public void setPath( String path )
	{
		this.path = path;
	}

	/**
	 * Load a properties file
	 * 
	 * @param path The path to the properties file
	 * @throws IOException
	 */
	public void load( String path ) throws IOException
	{
		if( path == null ){
			
			lager.info( "No application configuration specified!" );
			return;
		}
		
		File file = new File( path );
		if( file.exists() && file.lastModified() == lastModified )
			return;

		lager.debug( "loading config file: " + file );
		
		FileInputStream fis = new FileInputStream( file );
		byte[] buf = new byte[(int) file.length()];
		fis.read( buf );
		contents = new String( buf );
		
		this.clear();
		this.load( new StringReader( contents ) );
		
		fis.close();
		lastModified = file.lastModified();
	}

	/**
	 * Loads and parses <code>buffer</code>
	 * 
	 * @param buffer The properties file buffer.
	 * @throws IOException
	 */
	public void load( StringBuffer buffer ) throws IOException
	{
		StringReader in = new StringReader( buffer.toString() );
		load( in );
		in.close();
	}
	
	/**
	 * Get the configuration properties as a <code>java.util.Map</code>
	 * 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public Map<String, Object> asMap()
	{
		Map<String, Object> result = new HashMap<String, Object>();
		Enumeration<String> keys = (Enumeration<String>) propertyNames();

		while( keys.hasMoreElements() )
		{
			String key = keys.nextElement();
			String value = getProperty( key );

			if( value.trim().equals( "true" ) || value.trim().equals( "false" ) )
			{
				try
				{
					Boolean b = Boolean.valueOf( value );
					result.put( key, b );
					continue;
				}
				catch( Exception e )
				{
				}
			}

			try
			{
				Integer b = new Integer( value );
				result.put( key, b );
				continue;
			}
			catch( Exception e )
			{
			}

			result.put( key, value.replaceAll( "\"", "" ) );
		}

		return result;
	}
	
	/**
	 * Get the configuration properties as a javascript
	 * associative array.
	 * 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public String asJavascript()
	{
		try
		{
			load( this.path );
		}
		catch( IOException e )
		{
			e.printStackTrace();
		}

		StringBuffer result = new StringBuffer();
		StringBuffer namespaces = new StringBuffer();

		Enumeration<String> names = (Enumeration<String>) propertyNames();
		List<String> parents = new ArrayList<String>();

		while( names.hasMoreElements() )
		{
			String name = names.nextElement();
			String value = getProperty( name );

			if( name.startsWith( "client." ) )
			{
				String parent = name.substring( 0, name.lastIndexOf( "." ) );
				if( !parents.contains( parent ) )
				{
					parents.add( parent );
				}
				String js = "properties[\"" + name + "\"]=" + value + ";\n";
				result.append( js );
			}
		}

		return "\n" + namespaces + result;
	}

	/**
	 * @return the contents
	 */
	public String getContents()
	{
		return contents;
	}

	/**
	 * @param contents
	 *            the contents to set
	 */
	public void setContents( String contents )
	{
		this.contents = contents;
	}
	
	/**
	 * Get a string property.  If the property is a client property and
	 * prefixed with the "client" namespace, passing in an optional 2nd 
	 * argument as "true" will trim the string and parse out the quotes.
	 * 
	 * @param key
	 * @param parseQuotes
	 * @return
	 */
	public String getStringProperty( String key, Boolean ...  parseQuotes )
	{
		boolean removeQuotes = parseQuotes.length == 0?parseQuotes[0]:true;
		String value = getProperty( key );
		
		if( value != null )
		{
			value = value.trim();
			
			char fc = value.charAt( 0 );
			char lc = value.charAt( value.length()-1 );
			
			boolean isValid = (fc == '"' || fc == '\'') && (lc == '"' || lc == '\''); 
			
			if( removeQuotes && isValid )
			{
				value = value.substring( 1, value.length()-1 );
			}
		}
		
		return value;
	}
	
	/**
	 * Get a boolean property.  Simply wraps
	 * <code>
	 * Boolean.parseBoolean( str )
	 * </code>
	 * 
	 * @param key
	 * @return
	 */
	public boolean getBooleanProperty( String key )
	{
		return Boolean.parseBoolean( getProperty(key) );
	}
	
	/**
	 * Get a numeric property.  Passing in an optional 2nd argument
	 * boolean will return the number as a Float, other wise it's returned
	 * as an Integer.
	 * 
	 * @param key
	 * @return
	 */
	public Number getNumericProperty( String key, Boolean ...  isFloat  )
	{
		boolean isInteger = isFloat.length == 0?true:isFloat[0];
		
		if( isInteger )
		{
			return Integer.parseInt( getProperty(key) );
		}
		else
		{
			return Float.parseFloat( getProperty(key) );
		}
	}
}
